﻿// This file is part of the DynObjects library.
// Copyright 2010 Stefan Dragnev.
// The library and all its files are distributed under the MIT license.
// See the full text of the license in the accompanying LICENSE.txt file or at http://www.opensource.org/licenses/mit-license.php

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using System.Text.RegularExpressions;
using System.Xml;

namespace ComponentAccessTool
{
    class Program
    {
        private static bool ParseXmlBool(string boolStr)
        {
            if (String.IsNullOrEmpty(boolStr))
                return false;
            if (boolStr == "1")
                return true;
            if (boolStr == "0")
                return false;

            return bool.Parse(boolStr);
        }

        private static IComparer<T> CreateComparer<T>(Func<T, T, int> comparer)
        {
            return new LambdaComparer<T>(comparer);
        }

        private const string Delimiter = "////////////////////////////////////////////////////////////////////////////";

        static void Main(string[] args)
        {
            var fileName = args[0];
            var outFileName = Path.GetFileNameWithoutExtension(fileName) + ".dyn.cs";

            try
            {
                var code = new Program().Process(fileName);
                File.WriteAllText(outFileName, code);
            }
            catch (Exception ex)
            {
                File.WriteAllText(outFileName, ex.ToString());
            }
        }

        XPathNavigator myNavi;
        List<string> myOutput;

        public string Process(string dynFileName)
        {
            myOutput = new List<string>();
            var doc = new XmlDocument();
            doc.Load(new XmlTextReader(dynFileName));
            myNavi = doc.CreateNavigator();

            var @namespace = myNavi.SelectSingleNode("/Dyn/RootNamespace").Value.Trim();
            var classPrefix = myNavi.SelectSingleNode("/Dyn/ComponentProvider").Value.Trim();

            var components = (from XPathNavigator comp in myNavi.Select("/Dyn/Components/*")
                              orderby comp.Value
                              select comp.Value.Trim()).ToArray();

            myOutput.Add(String.Format(Delimiter));

            var customInfoNode = myNavi.SelectSingleNode("/Dyn/InfoMessage");
            var customInfo = customInfoNode != null ? customInfoNode.Value : null;
            if (customInfo != null)
            {
                myOutput.Add(String.Format("// {0}", customInfo));
                myOutput.Add(String.Format(Delimiter));
            }
            
            myOutput.Add(String.Format("// Generated code, do not modify; instead, modify the source .dyn file."));
            myOutput.Add(String.Format("// Source file: {0}", dynFileName));
            //myOutput.Add(String.Format("// Generated at: {0}", DateTime.Now));
            //myOutput.Add(String.Format("// Generated by: {0}", System.Security.Principal.WindowsIdentity.GetCurrent().Name));
            myOutput.Add(String.Format(Delimiter));
            myOutput.Add("");

            WriteUsingDirectives();
            myOutput.Add(string.Empty);

            myOutput.Add(String.Format("namespace {0}", @namespace));
            myOutput.Add("{");

            WriteMessages(classPrefix);
            myOutput.Add(string.Empty);
            
            WriteComponentProvider(classPrefix, components);
            myOutput.Add(string.Empty);
            WriteComponentAccessors(classPrefix, components);

            myOutput.Add("}"); // namespace end
            return String.Join("\n", myOutput.ToArray());
        }

        private void WriteUsingDirectives()
        {
            var namespaces = from XPathNavigator ns in myNavi.Select("/Dyn/UsingNamespaces/Namespace") select ns.Value.Trim();

            foreach (var ns in namespaces.OrderBy(s => s, CreateComparer<string>(
                (a, b) =>
                    {
                        var aSystem = a.StartsWith("System.") || a == "System";
                        var bSystem = b.StartsWith("System.") || b == "System";
                        if (aSystem && !bSystem)
                            return -1;
                        else if (!aSystem && bSystem)
                            return 1;
                        else return StringComparer.Ordinal.Compare(a, b);
                    })))
                myOutput.Add(string.Format("using {0};", ns));
        }

        private void WriteMessages(string provider)
        {
            var allMessages = from XPathNavigator msg in myNavi.Select("/Dyn/Messages/*")
                              orderby ParseMessageSig(msg).OverloadedMessageName
                              select msg;
            var defsNamespace = myNavi.SelectSingleNode("/Dyn/MessageDefsNamespace").Value.Trim();

            var defaultImplClass = myNavi.SelectSingleNode("/Dyn/DefaultMessageImplementationClass").Value.Trim();
            myOutput.Add("\t[Dyn.DefaultMessageImplementer]");
            myOutput.Add(string.Format("\tpublic sealed partial class {0}", defaultImplClass));

            bool isFirst = true;
            foreach (XPathNavigator msg in allMessages)
            {
                myOutput.Add(string.Format("\t\t{2} {0}.{1}", defsNamespace, ParseMessageSig(msg).OverloadedMessageName,
                                           isFirst ? ':' : ','));
                isFirst = false;
            }
            myOutput.Add("\t{");

            bool first = true;
            foreach (XPathNavigator msg in allMessages)
            {
                if (!first)
                    myOutput.Add(string.Empty);
                first = false;
                var sig = ParseMessageSig(msg);

                var multicast = ParseXmlBool(msg.GetAttribute("Multicast", ""));
                var hasDefaultImpl = ParseXmlBool(msg.GetAttribute("HasDefaultImpl", string.Empty));
                if (hasDefaultImpl)
                {
                    myOutput.Add(string.Format("\t\t//public {0}", sig.Signature));
                    myOutput.Add("\t\t//{");
                    myOutput.Add("\t\t//}");
                }
                else
                {
                    myOutput.Add(string.Format("\t\tpublic {0}", sig.Signature));
                    myOutput.Add("\t\t{");
                    if (!multicast)
                        myOutput.Add(String.Format("\t\t\tthrow new Dyn.DynException(\"Unimplemented message: {0}.{1}\");", defsNamespace, sig.OverloadedMessageName));
                    else
                        myOutput.Add("\t\t\t// multicast message");
                    myOutput.Add("\t\t}");
                }
            }
            myOutput.Add("\t}");
            myOutput.Add(string.Empty);

            myOutput.Add(String.Format("\tpublic static class {0}", defsNamespace));
            myOutput.Add("\t{");

            var delgsNamespace = defsNamespace + "Delegates";

            int messageIdx = 0;
            foreach (var msg in allMessages)
            {
                var multicast = ParseXmlBool(msg.GetAttribute("Multicast", string.Empty));
                var msgSig = ParseMessageSig(msg);

                var delegateLink = string.Empty;
                if (!msgSig.CallThruInterface)
                    delegateLink = String.Format(", Delegate = typeof({0}.{1})", delgsNamespace, msgSig.OverloadedMessageName);

                myOutput.Add(String.Format("\t\t[Dyn.Message(Index = {0}{1}{2}{3})]"
                    , messageIdx, multicast ? ", Multicast = true" : ""
                    , msgSig.IsGeneric ? ", ThruInterface = true" : ""
                    , delegateLink));
                myOutput.Add(String.Format("\t\tpublic interface {0}", msgSig.OverloadedMessageName));
                myOutput.Add("\t\t{");
                myOutput.Add(String.Format("\t\t\t{0};", msgSig.Signature));
                myOutput.Add("\t\t}");
                messageIdx++;
            }
            myOutput.Add("");
            myOutput.Add("\t\tpublic static int _Offset;");
            myOutput.Add("\t}");
            myOutput.Add(string.Empty);

            myOutput.Add(String.Format("\tpublic static class {0}", delgsNamespace));
            myOutput.Add("\t{");
            foreach (XPathNavigator msg in allMessages)
            {
                var msgSig = ParseMessageSig(msg);
                if (!msgSig.CallThruInterface)
                {
                    myOutput.Add(string.Format("\t\tpublic delegate {0} {1}(Dyn.DynObject _obj{2});",
                        msgSig.ReturnType, msgSig.OverloadedMessageName, msgSig.MakeTrailingParameters()));
                }
            }
            myOutput.Add("\t}");
            myOutput.Add(string.Empty);

            myOutput.Add(String.Format("\tpublic static class {0}Extensions", provider));
            myOutput.Add("\t{");
            messageIdx = 0;
            foreach (var msg in allMessages)
            {
                if (messageIdx > 0)
                    myOutput.Add("");

                var sig = ParseMessageSig(msg);

                var messageCaller = new List<string>(10);
                var messageNextBidderCaller = new List<string>(10);

                foreach (var attribute in sig.Attributes)
                {
                    messageCaller.Add("\t\t" + attribute);
                }
                messageCaller.Add("\t\t[System.Diagnostics.DebuggerNonUserCode]");
                messageCaller.Add(String.Format("\t\tpublic static {0} {1}{4}(this Dyn.DynObject _obj{2}{3}){5}",
                    sig.ReturnType, sig.MessageName, sig.Parameters != string.Empty ? ", " : string.Empty,
                    sig.Parameters, sig.GenericParameters, sig.GenericConstraints));
                messageCaller.Add("\t\t{");

                messageNextBidderCaller.Add("\t\t[System.Diagnostics.DebuggerNonUserCode]");
                messageNextBidderCaller.Add(String.Format("\t\tpublic static {0} {1}ForNextBidder{4}(this Dyn.DynObject _obj, Type currentMessageImplementor{2}{3}){5}",
                    sig.ReturnType, sig.MessageName, sig.Parameters != string.Empty ? ", " : string.Empty,
                    sig.Parameters, sig.GenericParameters, sig.GenericConstraints));
                messageNextBidderCaller.Add("\t\t{");

                var isMulticast = ParseXmlBool(msg.GetAttribute("Multicast", ""));

                var offsetStr = String.Format("{0}._Offset + {1}", defsNamespace, messageIdx);
                if (sig.CallThruInterface)
                {
                    if (!isMulticast)
                    {
                        messageCaller.Add(String.Format("\t\t\t{3}(({0}.{1})_obj._Components[_obj._UnicastImpl[{6}]]).{5}{4}({2});",
                            defsNamespace, sig.OverloadedMessageName, sig.CreateParameterPassingString(false),
                            sig.MakeReturnStmt(), sig.GenericParameters, sig.MessageName, offsetStr));

                        messageNextBidderCaller.Add(String.Format("\t\t\t{3}(({0}.{1})_obj._Components[_obj.TypeInfo.GetNextMessageBidderInterface({6}, currentMessageImplementor)]).{5}{4}({2});",
                            defsNamespace, sig.OverloadedMessageName, sig.CreateParameterPassingString(false),
                            sig.MakeReturnStmt(), sig.GenericParameters, sig.MessageName, offsetStr));
                    }
                    else
                    {
                        messageCaller.Add(String.Format("\t\t\tforeach (var impl in _obj.GetMulticastMessageImplementers({0}))", offsetStr));
                        messageCaller.Add("\t\t\t{");
                        messageCaller.Add(String.Format("\t\t\t\t(({0}.{1})impl).{3}({2});", defsNamespace,
                            sig.OverloadedMessageName, sig.CreateParameterPassingString(false), sig.MessageName));
                        messageCaller.Add("\t\t\t}");
                    }
                }
                else
                {
                    messageCaller.Add(String.Format("\t\t\t{0}(({1}.{2})_obj._MessageImpl[{4}])(_obj{3});", sig.MakeReturnStmt(),
                        delgsNamespace, sig.OverloadedMessageName, sig.CreateParameterPassingString(true), offsetStr));

                    messageNextBidderCaller.Add(String.Format("\t\t\t{0}(({1}.{2})_obj.TypeInfo.GetNextMessageBidderDelegate({4}, currentMessageImplementor))(_obj{3});",
                        sig.MakeReturnStmt(), delgsNamespace, sig.OverloadedMessageName, sig.CreateParameterPassingString(true), offsetStr));
                }
                messageCaller.Add("\t\t}");
                messageNextBidderCaller.Add("\t\t}");

                myOutput.AddRange(messageCaller);
                if (!isMulticast)
                    myOutput.AddRange(messageNextBidderCaller);

                messageIdx++;
            }
            myOutput.Add("\t}");

            if (myNavi.SelectSingleNode("/Dyn/Components/MulticastBeacon") != null)
            {
                myOutput.Add(string.Empty);
                WriteMulticastBeacon(allMessages, defsNamespace, delgsNamespace);
            }
        }

        private void WriteMulticastBeacon(IEnumerable<XPathNavigator> messages, string defsNamespace, string delgsNamespace)
        {
            var beaconComponentData = myNavi.SelectSingleNode("/Dyn/Components/MulticastBeacon");
            var beaconName = beaconComponentData.Value;
            var isOptOut = ParseXmlBool(beaconComponentData.GetAttribute("OptOut", ""));

            myOutput.Add(String.Format("\tpublic class {0} : Dyn.Component", beaconName));

            var messagesWithBeaconQ = from msg in messages
                                     where ParseXmlBool(msg.GetAttribute("Multicast", ""))
                                     let beaconAttr = msg.GetAttribute("GenerateBeacon", "")
                                     let msgWantsBeacon = !String.IsNullOrEmpty(beaconAttr) ? (bool?) ParseXmlBool(beaconAttr) : null
                                     where (isOptOut && msgWantsBeacon != false) || (!isOptOut && msgWantsBeacon == true)
                                     select msg;
            var messagesWithBeacon = messagesWithBeaconQ.ToArray();

            foreach (var msg in messagesWithBeacon)
            {
                myOutput.Add(string.Format("\t\t, {0}.{1}", defsNamespace, ParseMessageSig(msg).OverloadedMessageName));
            }

            myOutput.Add("\t{");

            foreach (var msg in messagesWithBeacon)
            {
                var messageName = ParseMessageSig(msg).OverloadedMessageName;
                myOutput.Add(String.Format("\t\tpublic event {0}.{1} {1}Beacon;", delgsNamespace, messageName));
            }
            
            foreach (var msg in messagesWithBeacon)
            {
                myOutput.Add(string.Empty);

                var sig = ParseMessageSig(msg);
                myOutput.Add(String.Format("\t\tpublic {0}", sig.Signature));
                myOutput.Add("\t\t{");
                myOutput.Add(String.Format("\t\t\tvar evt = {0}Beacon;", sig.OverloadedMessageName));
                myOutput.Add(String.Format("\t\t\tif (evt != null)"));
                myOutput.Add(String.Format("\t\t\t\tevt(DynThis{0});", sig.CreateParameterPassingString(true)));
                myOutput.Add("\t\t}");
            }

            myOutput.Add("\t}");
        }

        private void WriteComponentAccessors(string provider, string[] components)
        {
            myOutput.Add(String.Format("\tpublic static class {0}Accessors", provider));
            myOutput.Add("\t{");

            bool supportContracts = GetSupportContracts();

            var totalGetters = 0;
            for (var i = 0; i < components.Length; ++i)
            {
                if (totalGetters > 0)
                    myOutput.Add("");

                var typeSafeName = components[i].Replace('.', '_');
                myOutput.Add(String.Format("\t\tpublic static int _{0}Offset = {1};", typeSafeName, i));

                var componentDesc = myNavi.SelectSingleNode(string.Format("/Dyn/Components/*[text()='{0}']", components[i]));
                if (ParseXmlBool(componentDesc.GetAttribute("NoAccessor", "")))
                    continue;

                var accessorName = components[i];
                var accessorOverride = componentDesc.GetAttribute("Accessor", "");
                if (!String.IsNullOrEmpty(accessorOverride))
                {
                    accessorName = accessorOverride;
                }
                else
                {
                    var dotIndex = accessorName.LastIndexOf('.');
                    if (dotIndex >= 0)
                        accessorName = accessorName.Substring(dotIndex + 1);
                }
                myOutput.Add("\t\t[System.Diagnostics.DebuggerNonUserCode]");
                if (supportContracts)
                    myOutput.Add("\t\t[System.Diagnostics.Contracts.Pure]");
                myOutput.Add(String.Format("\t\tpublic static {0} Get{1}(this Dyn.DynObject obj)", components[i], accessorName));
                myOutput.Add("\t\t{");
                myOutput.Add(String.Format("\t\t\treturn obj.GetComponent(_{1}Offset) as {0};", components[i], typeSafeName));
                myOutput.Add("\t\t}");

                totalGetters++;
            }
            myOutput.Add("\t}");
        }

        private bool GetSupportContracts()
        {
            var supportContractsElem = myNavi.SelectSingleNode("/Dyn/@SupportContracts");
            return supportContractsElem == null || ParseXmlBool(supportContractsElem.Value);
        }

        private void WriteComponentProvider(string provider, IEnumerable<string> components)
        {
            var defaultImplClass = myNavi.SelectSingleNode("/Dyn/DefaultMessageImplementationClass").Value.Trim();
            var defsNamespace = myNavi.SelectSingleNode("/Dyn/MessageDefsNamespace").Value.Trim();

            myOutput.Add(String.Format("\tpublic sealed class {0} : Dyn.ComponentProvider", provider));
            myOutput.Add("\t{");
            myOutput.Add(String.Format("\t\tpublic {0}()", provider));
            myOutput.Add("\t\t{");

            myOutput.Add(String.Format("\t\t\tMessageCount = {0};", myNavi.Select("/Dyn/Messages/*").Count));
            myOutput.Add(String.Format("\t\t\tComponentAccessors = typeof({0}Accessors);", provider));
            myOutput.Add(String.Format("\t\t\tMessageExtensions = typeof({0});", defsNamespace));
            myOutput.Add(String.Format("\t\t\tMessageDelegates = typeof({0}Delegates);", defsNamespace));
            myOutput.Add(String.Format("\t\t\tDefaultMsgImpls = new[] {{ new {0}() }};", defaultImplClass));
            myOutput.Add(string.Empty);

            myOutput.Add("\t\t\tTypes = new System.Type[]");
            myOutput.Add("\t\t\t\t{");
            foreach (var c in components)
            {
                myOutput.Add(String.Format("\t\t\t\t\ttypeof({0}),", c));
            }
            myOutput.Add("\t\t\t\t};");

            myOutput.Add("\t\t}");
            myOutput.Add("\t}");
        }

        public struct MessageSignature
        {
            public string[] Attributes;
            public string ReturnType;
            public string MessageName;
            public string OverloadedMessageName;
            public string GenericParameters;
            public string Parameters;
            public string GenericConstraints;
            public string Signature;
            public bool OverrideCallThruInterface;

            public List<Parameter> ParameterList;

            public struct Parameter
            {
                public string PassScheme;
                public string Type;
                public string Name;
            }

            public string CreateParameterPassingString(bool trailing)
            {
                if (Parameters.Length == 0)
                    return string.Empty;

                var result = new StringBuilder();
                foreach (var p in ParameterList)
                {
                    if (result.Length > 0 || trailing)
                        result.Append(", ");

                    if (p.PassScheme != "")
                        result.Append(p.PassScheme + " ");
                    result.Append(p.Name);
                }
                return result.ToString();
            }

            public string MakeSignature(string messageName)
            {
                return ReturnType + ' ' + messageName + GenericParameters + '(' + Parameters + ')' + GenericConstraints;
            }

            public string MakeTrailingParameters()
            {
                return Parameters == "" ? "" : ", " + Parameters;
            }

            public string MakeReturnStmt()
            {
                return ReturnType == "void" ? "" : "return ";
            }

            public bool IsGeneric
            {
                get
                {
                    return GenericParameters != "";
                }
            }

            public bool CallThruInterface
            {
                get
                {
                    return IsGeneric || OverrideCallThruInterface;
                }
            }
        }

        private static MessageSignature ParseMessageSig(XPathNavigator sigXml)
        {
            var sigStr = sigXml.Value;

            string[] attributes;
            sigStr = ExtractAttributes(sigStr, out attributes);

            sigStr = sigStr.Trim().Replace('{', '<').Replace('}', '>').TrimEnd(';');

            var r = new Regex(@"^(.+?) ([A-Za-z0-9_]+?)(\<[A-Za-z0-9_, ]+?\>)?\((.*?)\)(.*)$");
            var match = r.Match(sigStr);
            if (match.Success)
            {
                var sig = new MessageSignature();
                sig.Signature = sigStr;
                sig.Attributes = attributes;
                sig.ReturnType = match.Groups[1].Value.Trim();
                sig.MessageName = match.Groups[2].Value.Trim();
                sig.GenericParameters = match.Groups[3].Value.Trim();
                sig.Parameters = match.Groups[4].Value.Trim();
                sig.GenericConstraints = match.Groups[5].Value.TrimEnd();
                sig.OverrideCallThruInterface = ParseXmlBool(sigXml.GetAttribute("CallThruInterface", ""));

                sig.ParameterList = ParseParameters(sig.Parameters);
                sig.OverloadedMessageName = sig.MessageName + sigXml.GetAttribute("Overload", "");

                return sig;
            }
            else
                throw new ApplicationException(String.Format("Cannot parse message signature: {0}", sigStr));
        }

        private static string ExtractAttributes(string sigStr, out string[] attributes)
        {
            var attrs = new List<string>();
            while (true)
            {
                sigStr = sigStr.Trim();
                if (sigStr.FirstOrDefault() != '[')
                {
                    attributes = attrs.ToArray();
                    return sigStr;
                }

                var index = 1;
                for (var nesting = 1; nesting != 0 && index < sigStr.Length; index++)
                {
                    switch (sigStr[index])
                    {
                        case '[':
                            nesting++;
                            break;
                        case ']':
                            nesting--;
                            break;
                    }
                }

                attrs.Add(sigStr.Substring(0, index));
                sigStr = sigStr.Substring(index);
            }
        }

        private static List<MessageSignature.Parameter> ParseParameters(string paramStr)
        {
            var parameters = new List<MessageSignature.Parameter>();
            if (paramStr == "")
                return parameters;

            var startPos = 0;
            var nested = 0;
            for (var pos = 0; pos < paramStr.Length; ++pos)
            {
                switch (paramStr[pos])
                {
                    case '<':
                        nested++;
                        break;
                    case '>':
                        nested--;
                        if (nested < 0) throw new ApplicationException(String.Format("Invalid parameter string at position {1}: ({0})", paramStr, pos));
                        break;
                    case ',':
                        if (nested == 0)
                        {
                            var param = GenerateParameter(paramStr, startPos, pos);
                            parameters.Add(param);
                            startPos = pos + 1;
                        }
                        break;
                }
            }

            if (nested != 0)
                throw new ApplicationException(String.Format("Invalid parameter string: ({0})", paramStr));
            parameters.Add(GenerateParameter(paramStr, startPos, paramStr.Length));
            

            return parameters;
        }

        private static MessageSignature.Parameter GenerateParameter(string paramStr, int startPos, int endPos)
        {
            var param = new MessageSignature.Parameter();
            var paramSig = paramStr.Substring(startPos, endPos - startPos);
            var tokens = paramSig.Trim().Split(' ').ToList();
            var typeIndex = (tokens[0] == "out" || tokens[0] == "ref") ? 1 : 0;
            param.PassScheme = typeIndex == 1 ? tokens[0] : "";
            param.Name = tokens.Last();
            var typeTokens = tokens.Skip(typeIndex).Take(tokens.Count - 1 - typeIndex);
            param.Type = String.Join("", typeTokens.ToArray());
            return param;
        }
    }

    internal class LambdaComparer<T> : IComparer<T>
    {
        private readonly Func<T, T, int> myComparer;

        public LambdaComparer(Func<T, T, int> comparer)
        {
            myComparer = comparer;
        }

        public int Compare(T x, T y)
        {
            return myComparer(x, y);
        }
    }
}
